/* $Id: XMLWriter.java 39 2009-07-24 22:48:06Z h_m_r@gmx.de $ */
package util;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

public class XMLWriter {

	private Writer writer;
	private Stack<String> elementNames = new Stack<String>();

	private State state;
	private String indent;

	private static final Map<String, String> escapeReplacements;

	enum State {
		WITHOUT_ROOT_NODE, ATTRIBUTE_LIST, INSIDE_NODE, // inside non empty node
		OUTSIDE_ROOT_NODE
	}

	public class XMLWriterException extends Exception {
		private static final long serialVersionUID = -8030251054685997866L;

		public XMLWriterException() {
			super();
		}

		public XMLWriterException(String message) {
			super(message);
		}
	}

	public XMLWriter(Writer writer) {
		this(writer, null);
	}

	public XMLWriter(Writer writer, String indent) {
		this.writer = writer;
		this.indent = indent;
		state = State.WITHOUT_ROOT_NODE;
	}

	public XMLWriter addElement(String elementName) throws IOException,
			XMLWriterException {
		return addElement(elementName, null);
	}

	public XMLWriter addElement(String elementName,
			Map<String, Object> attributes) throws IOException, XMLWriterException {
		return beginElement(elementName).addAttributes(attributes).endElement();
	}

	public XMLWriter beginElement(String elementName) throws IOException, XMLWriterException {

		if (state == State.OUTSIDE_ROOT_NODE) {
			throw new XMLWriterException(
					"Cannot add element outside of root node.");
		}

		// if currently in attribute-list => close current begin-tag first
		if (state == State.ATTRIBUTE_LIST) {
			writer.write(">");
		}

		indent();

		elementName = escape(elementName);
		elementNames.push(elementName);

		writer.write("<");
		writer.write(elementName);

		state = State.ATTRIBUTE_LIST;

		return this;
	}

	public XMLWriter endElement() throws IOException, XMLWriterException {

		if (elementNames.size() == 0)
			throw new XMLWriterException("Cannot close any element.");

		String elementName = elementNames.pop();

		if (state == State.ATTRIBUTE_LIST) {
			// currently in attribute-list => close element without content
			writer.write("/>");
		} else {
			// close element with content

			indent();

			writer.write("</");
			writer.write(elementName);
			writer.write(">");
		}

		if (elementNames.size() == 0) {
			state = State.OUTSIDE_ROOT_NODE;
		} else {
			state = State.INSIDE_NODE;
		}

		return this;
	}

	public XMLWriter addAttribute(String name) throws IOException, XMLWriterException {
		return addAttribute(name, null);
	}

	public XMLWriter addAttributes(Map<String, Object> attributes)
			throws IOException, XMLWriterException {
		if (attributes != null) {
			Iterator<Map.Entry<String, Object>> it = attributes.entrySet().iterator();
			while(it.hasNext()) {
				Map.Entry<String, Object> entry = it.next();
				addAttribute(entry.getKey(), entry.getValue());
			}
		}
		return this;
	}

	public XMLWriter addAttribute(String name, Object value) throws IOException, XMLWriterException {

		if (state != State.ATTRIBUTE_LIST) {
			throw new XMLWriterException(
					"Cannot add attributes outside of opening tag.");
		}

		name = escape(name);

		writer.write(" ");
		writer.write(name);
		writer.write("=\"");
		if (value == null) {
			writer.write(name);
		} else {
			writer.write(escape(value.toString()));
		}
		writer.write("\"");

		return this;
	}

	public XMLWriter addText(String text) throws IOException, XMLWriterException {

		if (state == State.OUTSIDE_ROOT_NODE) {
			throw new XMLWriterException(
					"Cannot add text outside of root node.");
		}

		// if currently in attribute-list => close current begin-tag first
		if (state == State.ATTRIBUTE_LIST) {
			writer.write(">");
		}

		indent(escape(text));

		state = State.INSIDE_NODE;

		return this;
	}

	private void indent() throws IOException {
		// add indention if wanted
		if(indent != null && state != State.WITHOUT_ROOT_NODE) {
			writer.write("\n");
			for(int i=0; i<elementNames.size(); ++i)
				writer.write(indent);
		}
	}

	private void indent(String text) throws IOException {
		if(indent == null) {
			writer.write(text);
		}
		else {
			String[] lines = text.split("\n");
			for(String line: lines) {
				indent();
				writer.write(line);
			}
		}
	}


	public XMLWriter addTab() throws IOException, XMLWriterException {
		return addText("\t");
	}

	public XMLWriter addNewLine() throws IOException, XMLWriterException {
		return addText("\n");
	}

	public Writer getWriter() {
		return writer;
	}

	static {
		escapeReplacements = new HashMap<String, String>();
		escapeReplacements.put("&", "&amp;");
		escapeReplacements.put("<", "&lt;");
		escapeReplacements.put(">", "&gt;");
		escapeReplacements.put("\"", "&quot;");
		escapeReplacements.put("'", "&apos;");
	}

	static public String escape(String string) {
		Iterator<Map.Entry<String, String>> it = escapeReplacements.entrySet().iterator();
		while(it.hasNext()) {
			Map.Entry<String, String> entry = it.next();
			string = string.replaceAll(entry.getKey(), entry.getValue());
		}
		return string;
	}



}
